From a40d3b03b8afb06187447c7607c928e13241f846 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 28 Jan 2015 19:37:19 -0800 Subject: [PATCH] Consider transitive fingerprints for freshness Originally discovered through #1236, this commit fixes a bug in Cargo where crates may not be recompiled when they need to (leading to obscure errors from the compiler). The scenario in question looks like: * Assume a dependency graph of `A -> B -> C` and `A -> C` * Build all packages * Modify C * Rebuild, but hit Ctrl+C while B is building * Modify A * Rebuild again Previously, Cargo only considered the freshness of a package to be the freshness of the package itself (checking source files, for example). To handle transitive recompilations, Cargo propagates a dirty bit throughout the dependency graph automatically (instead if calculating it as such). In the above example, however, we have a problem where as part of the last rebuild Cargo thinks `B` and `C` are fresh! The artifact for `C` was just recompiled, but `B`'s source code is untainted, so Cargo does not think that it needs to recompile `B`. This is wrong, however, because one of `B`'s dependencies was rebuilt, so it needs to be rebuilt. To fix this problem, the fingerprint (a short hash) for all packages is now transitively propagated (the fingerprint changes when an upstream package changes). This should ensure that even when Ctrl+C is hit (or the situation explained in #1236) that Cargo will still consider packages whose source code is untainted as candidates for recompilation. The implementation is somewhat tricky due to the actual fingerprint for a path dependency not being known until *after* the crate is compiled (the fingerprint is the mtime of the dep-info file). Closes #1236 --- src/cargo/core/manifest.rs | 11 +- src/cargo/core/package_id.rs | 2 +- src/cargo/ops/cargo_rustc/context.rs | 26 ++- src/cargo/ops/cargo_rustc/fingerprint.rs | 222 +++++++++++++++++------ src/cargo/ops/cargo_rustc/mod.rs | 10 +- tests/support/paths.rs | 2 + tests/test_cargo_cross_compile.rs | 7 +- tests/test_cargo_freshness.rs | 51 ++++++ tests/test_cargo_test.rs | 3 - 9 files changed, 252 insertions(+), 82 deletions(-) diff --git a/src/cargo/core/manifest.rs b/src/cargo/core/manifest.rs index 98365e899..424eb5a35 100644 --- a/src/cargo/core/manifest.rs +++ b/src/cargo/core/manifest.rs @@ -68,7 +68,7 @@ impl Encodable for Manifest { } } -#[derive(Debug, Clone, PartialEq, Hash, RustcEncodable, Copy)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, RustcEncodable, Copy)] pub enum LibKind { Lib, Rlib, @@ -103,14 +103,14 @@ impl LibKind { } } -#[derive(Debug, Clone, Hash, PartialEq, RustcEncodable)] +#[derive(Debug, Clone, Hash, PartialEq, RustcEncodable, Eq)] pub enum TargetKind { Lib(Vec), Bin, Example, } -#[derive(RustcEncodable, RustcDecodable, Clone, PartialEq, Debug)] +#[derive(RustcEncodable, RustcDecodable, Clone, PartialEq, Eq, Debug)] pub struct Profile { env: String, // compile, test, dev, bench, etc. opt_level: u32, @@ -345,7 +345,7 @@ impl hash::Hash for Profile { } /// Informations about a binary, a library, an example, etc. that is part of the package. -#[derive(Clone, Hash, PartialEq, Debug)] +#[derive(Clone, Hash, PartialEq, Eq, Debug)] pub struct Target { kind: TargetKind, name: String, @@ -467,7 +467,8 @@ impl Manifest { impl Target { pub fn file_stem(&self) -> String { match self.metadata { - Some(ref metadata) => format!("{}{}", self.name, metadata.extra_filename), + Some(ref metadata) => format!("{}{}", self.name, + metadata.extra_filename), None => self.name.clone() } } diff --git a/src/cargo/core/package_id.rs b/src/cargo/core/package_id.rs index d82ce75a4..0e1825adb 100644 --- a/src/cargo/core/package_id.rs +++ b/src/cargo/core/package_id.rs @@ -112,7 +112,7 @@ impl FromError for Box { fn from_error(t: PackageIdError) -> Box { Box::new(t) } } -#[derive(PartialEq, Hash, Clone, RustcEncodable, Debug)] +#[derive(PartialEq, Eq, Hash, Clone, RustcEncodable, Debug)] pub struct Metadata { pub metadata: String, pub extra_filename: String diff --git a/src/cargo/ops/cargo_rustc/context.rs b/src/cargo/ops/cargo_rustc/context.rs index 81573d8b4..10a59c1bd 100644 --- a/src/cargo/ops/cargo_rustc/context.rs +++ b/src/cargo/ops/cargo_rustc/context.rs @@ -5,14 +5,15 @@ use std::sync::Arc; use regex::Regex; -use core::{SourceMap, Package, PackageId, PackageSet, Resolve, Target}; +use core::{SourceMap, Package, PackageId, PackageSet, Resolve, Target, Profile}; use util::{self, CargoResult, ChainError, internal, Config, profile}; use util::human; -use super::{Kind, Compilation, BuildConfig}; use super::TargetConfig; -use super::layout::{Layout, LayoutProxy}; use super::custom_build::BuildState; +use super::fingerprint::Fingerprint; +use super::layout::{Layout, LayoutProxy}; +use super::{Kind, Compilation, BuildConfig}; use super::{ProcessEngine, ExecEngine}; #[derive(Debug, Copy)] @@ -29,6 +30,7 @@ pub struct Context<'a, 'b: 'a> { pub compilation: Compilation, pub build_state: Arc, pub exec_engine: Arc>, + pub fingerprints: HashMap<(&'a PackageId, &'a Target, Kind), Fingerprint>, env: &'a str, host: Layout, @@ -80,6 +82,7 @@ impl<'a, 'b: 'a> Context<'a, 'b> { build_state: Arc::new(BuildState::new(build_config.clone(), deps)), build_config: build_config, exec_engine: Arc::new(Box::new(ProcessEngine) as Box), + fingerprints: HashMap::new(), }) } @@ -357,6 +360,23 @@ impl<'a, 'b: 'a> Context<'a, 'b> { pub fn requested_target(&self) -> Option<&str> { self.build_config.requested_target.as_ref().map(|s| &s[]) } + + /// Calculate the actual profile to use for a target's compliation. + /// + /// This may involve overriding some options such as debug information, + /// rpath, opt level, etc. + pub fn profile(&self, target: &Target) -> Profile { + let mut profile = target.get_profile().clone(); + let root_package = self.get_package(self.resolve.root()); + for target in root_package.get_manifest().get_targets().iter() { + let root_profile = target.get_profile(); + if root_profile.get_env() != profile.get_env() { continue } + profile = profile.opt_level(root_profile.get_opt_level()) + .debug(root_profile.get_debug()) + .rpath(root_profile.get_rpath()) + } + profile + } } impl Platform { diff --git a/src/cargo/ops/cargo_rustc/fingerprint.rs b/src/cargo/ops/cargo_rustc/fingerprint.rs index 95752de97..f37f35cad 100644 --- a/src/cargo/ops/cargo_rustc/fingerprint.rs +++ b/src/cargo/ops/cargo_rustc/fingerprint.rs @@ -1,5 +1,4 @@ use std::collections::hash_map::Entry::{Occupied, Vacant}; -use std::hash::{Hash, Hasher, SipHasher}; use std::io::{self, fs, File, BufferedReader}; use std::io::fs::PathExtensions; @@ -39,56 +38,28 @@ pub type Preparation = (Freshness, Work, Work); /// This function will calculate the fingerprint for a target and prepare the /// work necessary to either write the fingerprint or copy over all fresh files /// from the old directories to their new locations. -pub fn prepare_target(cx: &mut Context, pkg: &Package, target: &Target, - kind: Kind) -> CargoResult { +pub fn prepare_target<'a, 'b>(cx: &mut Context<'a, 'b>, + pkg: &'a Package, + target: &'a Target, + kind: Kind) -> CargoResult { let _p = profile::start(format!("fingerprint: {} / {:?}", pkg.get_package_id(), target)); let new = dir(cx, pkg, kind); let loc = new.join(filename(target)); cx.layout(pkg, kind).proxy().whitelist(&loc); - // We want to use the package fingerprint if we're either a doc target or a - // path source. If we're a git/registry source, then the mtime of files may - // fluctuate, but they won't change so long as the source itself remains - // constant (which is the responsibility of the source) - let use_pkg = { - let doc = target.get_profile().is_doc(); - let path = pkg.get_summary().get_source_id().is_path(); - doc || !path - }; - info!("fingerprint at: {}", loc.display()); - // First bit of the freshness calculation, whether the dep-info file - // indicates that the target is fresh. - let dep_info = dep_info_loc(cx, pkg, target, kind); - let mut are_files_fresh = use_pkg || - try!(calculate_target_fresh(&dep_info)); - - // Second bit of the freshness calculation, whether rustc itself, the - // target are fresh, and the enabled set of features are all fresh. - let features = cx.resolve.features(pkg.get_package_id()); - let features = features.map(|s| { - let mut v = s.iter().collect::>(); - v.sort(); - v - }); - let rustc_fingerprint = if use_pkg { - mk_fingerprint(cx, &(target, try!(calculate_pkg_fingerprint(cx, pkg)), - features)) - } else { - mk_fingerprint(cx, &(target, features)) - }; - let is_rustc_fresh = try!(is_fresh(&loc, rustc_fingerprint.as_slice())); + let fingerprint = try!(calculate(cx, pkg, target, kind)); + let is_fresh = try!(is_fresh(&loc, &fingerprint)); let root = cx.out_dir(pkg, kind, target); + let mut missing_outputs = false; if !target.get_profile().is_doc() { for filename in try!(cx.target_filenames(target)).iter() { let dst = root.join(filename); cx.layout(pkg, kind).proxy().whitelist(&dst); - if are_files_fresh && !dst.exists() { - are_files_fresh = false; - } + missing_outputs |= !dst.exists(); if target.get_profile().is_test() { cx.compilation.tests.push((target.get_name().to_string(), dst)); @@ -104,7 +75,138 @@ pub fn prepare_target(cx: &mut Context, pkg: &Package, target: &Target, } } - Ok(prepare(is_rustc_fresh && are_files_fresh, loc, rustc_fingerprint)) + Ok(prepare(is_fresh && !missing_outputs, loc, fingerprint)) +} + +/// A fingerprint can be considered to be a "short string" representing the +/// state of a world for a package. +/// +/// If a fingerprint ever changes, then the package itself needs to be +/// recompiled. Inputs to the fingerprint include source code modifications, +/// compiler flags, compiler version, etc. This structure is not simply a +/// `String` due to the fact that some fingerprints cannot be calculated lazily. +/// +/// Path sources, for example, use the mtime of the corresponding dep-info file +/// as a fingerprint (all source files must be modified *before* this mtime). +/// This dep-info file is not generated, however, until after the crate is +/// compiled. As a result, this structure can be thought of as a fingerprint +/// to-be. The actual value can be calculated via `resolve()`, but the operation +/// may fail as some files may not have been generated. +/// +/// Note that dependencies are taken into account for fingerprints because rustc +/// requires that whenever an upstream crate is recompiled that all downstream +/// dependants are also recompiled. This is typically tracked through +/// `DependencyQueue`, but it also needs to be retained here because Cargo can +/// be interrupted while executing, losing the state of the `DependencyQueue` +/// graph. +#[derive(Clone)] +pub struct Fingerprint { + extra: String, + deps: Vec, + personal: Personal, +} + +#[derive(Clone)] +enum Personal { + Known(String), + Unknown(Path), +} + +impl Fingerprint { + fn resolve(&self) -> CargoResult { + let mut deps: Vec<_> = try!(self.deps.iter().map(|s| s.resolve()).collect()); + deps.sort(); + let known = match self.personal { + Personal::Known(ref s) => s.clone(), + Personal::Unknown(ref p) => { + debug!("resolving: {}", p.display()); + try!(fs::stat(p)).modified.to_string() + } + }; + debug!("inputs: {} {} {:?}", known, self.extra, deps); + Ok(util::short_hash(&(known, &self.extra, &deps))) + } +} + +/// Calculates the fingerprint for a package/target pair. +/// +/// This fingerprint is used by Cargo to learn about when information such as: +/// +/// * A non-path package changes (changes version, changes revision, etc). +/// * Any dependency changes +/// * The compiler changes +/// * The set of features a package is built with changes +/// * The profile a target is compiled with changes (e.g. opt-level changes) +/// +/// Information like file modification time is only calculated for path +/// dependencies and is calculated in `calculate_target_fresh`. +fn calculate<'a, 'b>(cx: &mut Context<'a, 'b>, + pkg: &'a Package, + target: &'a Target, + kind: Kind) + -> CargoResult { + let key = (pkg.get_package_id(), target, kind); + match cx.fingerprints.get(&key) { + Some(s) => return Ok(s.clone()), + None => {} + } + + // First, calculate all statically known "salt data" such as the profile + // information (compiler flags), the compiler version, activated features, + // and target configuration. + let features = cx.resolve.features(pkg.get_package_id()); + let features = features.map(|s| { + let mut v = s.iter().collect::>(); + v.sort(); + v + }); + let extra = util::short_hash(&(cx.config.rustc_version(), target, &features, + cx.profile(target))); + + // Next, recursively calculate the fingerprint for all of our dependencies. + let deps = try!(cx.dep_targets(pkg, target).into_iter().map(|(p, t)| { + let kind = match kind { + Kind::Host => Kind::Host, + Kind::Target if t.get_profile().is_for_host() => Kind::Host, + Kind::Target => Kind::Target, + }; + calculate(cx, p, t, kind) + }).collect::>>()); + + // And finally, calculate what our own personal fingerprint is + let personal = if use_dep_info(pkg, target) { + let dep_info = dep_info_loc(cx, pkg, target, kind); + match try!(calculate_target_mtime(&dep_info)) { + Some(i) => Personal::Known(i.to_string()), + None => { + // If the dep-info file does exist (but some other sources are + // newer than it), make sure to delete it so we don't pick up + // the old copy in resolve() + let _ = fs::unlink(&dep_info); + Personal::Unknown(dep_info) + } + } + } else { + Personal::Known(try!(calculate_pkg_fingerprint(cx, pkg))) + }; + let fingerprint = Fingerprint { + extra: extra, + deps: deps, + personal: personal, + }; + cx.fingerprints.insert(key, fingerprint.clone()); + Ok(fingerprint) +} + + +// We want to use the mtime for files if we're a path source, but if we're a +// git/registry source, then the mtime of files may fluctuate, but they won't +// change so long as the source itself remains constant (which is the +// responsibility of the source) +fn use_dep_info(pkg: &Package, target: &Target) -> bool { + let doc = target.get_profile().is_doc(); + let path = pkg.get_summary().get_source_id().is_path(); + !doc && path } /// Prepare the necessary work for the fingerprint of a build command. @@ -139,9 +241,13 @@ pub fn prepare_build_cmd(cx: &mut Context, pkg: &Package, kind: Kind, info!("fingerprint at: {}", loc.display()); let new_fingerprint = try!(calculate_build_cmd_fingerprint(cx, pkg)); - let new_fingerprint = mk_fingerprint(cx, &new_fingerprint); + let new_fingerprint = Fingerprint { + extra: String::new(), + deps: Vec::new(), + personal: Personal::Known(new_fingerprint), + }; - let is_fresh = try!(is_fresh(&loc, new_fingerprint.as_slice())); + let is_fresh = try!(is_fresh(&loc, &new_fingerprint)); // The new custom build command infrastructure handles its own output // directory as part of freshness. @@ -178,9 +284,12 @@ pub fn prepare_init(cx: &mut Context, pkg: &Package, kind: Kind) /// Given the data to build and write a fingerprint, generate some Work /// instances to actually perform the necessary work. -fn prepare(is_fresh: bool, loc: Path, fingerprint: String) -> Preparation { - let write_fingerprint = Work::new(move |desc_tx| { - drop(desc_tx); +fn prepare(is_fresh: bool, loc: Path, fingerprint: Fingerprint) -> Preparation { + let write_fingerprint = Work::new(move |_| { + debug!("write fingerprint: {}", loc.display()); + let fingerprint = try!(fingerprint.resolve().chain_error(|| { + internal("failed to resolve a pending fingerprint") + })); try!(File::create(&loc).write_str(fingerprint.as_slice())); Ok(()) }); @@ -201,13 +310,17 @@ pub fn dep_info_loc(cx: &Context, pkg: &Package, target: &Target, return ret; } -fn is_fresh(loc: &Path, new_fingerprint: &str) -> CargoResult { +fn is_fresh(loc: &Path, new_fingerprint: &Fingerprint) -> CargoResult { let mut file = match File::open(loc) { Ok(file) => file, Err(..) => return Ok(false), }; let old_fingerprint = try!(file.read_to_string()); + let new_fingerprint = match new_fingerprint.resolve() { + Ok(s) => s, + Err(..) => return Ok(false), + }; log!(5, "old fingerprint: {}", old_fingerprint); log!(5, "new fingerprint: {}", new_fingerprint); @@ -215,17 +328,9 @@ fn is_fresh(loc: &Path, new_fingerprint: &str) -> CargoResult { Ok(old_fingerprint.as_slice() == new_fingerprint) } -/// Frob in the necessary data from the context to generate the real -/// fingerprint. -fn mk_fingerprint>(cx: &Context, data: &T) -> String { - let mut hasher = SipHasher::new_with_keys(0,0); - (cx.config.rustc_version(), data).hash(&mut hasher); - util::to_hex(hasher.finish()) -} - -fn calculate_target_fresh(dep_info: &Path) -> CargoResult { +fn calculate_target_mtime(dep_info: &Path) -> CargoResult> { macro_rules! fs_try { - ($e:expr) => (match $e { Ok(e) => e, Err(..) => return Ok(false) }) + ($e:expr) => (match $e { Ok(e) => e, Err(..) => return Ok(None) }) } let mut f = BufferedReader::new(fs_try!(File::open(dep_info))); // see comments in append_current_dir for where this cwd is manifested from. @@ -233,7 +338,7 @@ fn calculate_target_fresh(dep_info: &Path) -> CargoResult { let cwd = Path::new(&cwd[..cwd.len()-1]); let line = match f.lines().next() { Some(Ok(line)) => line, - _ => return Ok(false), + _ => return Ok(None), }; let line = line.as_slice(); let mtime = try!(fs::stat(dep_info)).modified; @@ -258,13 +363,13 @@ fn calculate_target_fresh(dep_info: &Path) -> CargoResult { Ok(stat) if stat.modified <= mtime => {} Ok(stat) => { info!("stale: {} -- {} vs {}", file, stat.modified, mtime); - return Ok(false) + return Ok(None) } - _ => { info!("stale: {} -- missing", file); return Ok(false) } + _ => { info!("stale: {} -- missing", file); return Ok(None) } } } - Ok(true) + Ok(Some(mtime)) } fn calculate_build_cmd_fingerprint(cx: &Context, pkg: &Package) @@ -300,6 +405,7 @@ fn filename(target: &Target) -> String { // what that directory was at the beginning of the file so we can know about it // next time. pub fn append_current_dir(path: &Path, cwd: &Path) -> CargoResult<()> { + debug!("appending {} <- {}", path.display(), cwd.display()); let mut f = try!(File::open_mode(path, io::Open, io::ReadWrite)); let contents = try!(f.read_to_end()); try!(f.seek(0, io::SeekSet)); diff --git a/src/cargo/ops/cargo_rustc/mod.rs b/src/cargo/ops/cargo_rustc/mod.rs index 20e72393a..4b202c520 100644 --- a/src/cargo/ops/cargo_rustc/mod.rs +++ b/src/cargo/ops/cargo_rustc/mod.rs @@ -594,15 +594,7 @@ fn build_base_args(cx: &Context, // Despite whatever this target's profile says, we need to configure it // based off the profile found in the root package's targets. - let mut profile = target.get_profile().clone(); - let root_package = cx.get_package(cx.resolve.root()); - for target in root_package.get_manifest().get_targets().iter() { - let root_profile = target.get_profile(); - if root_profile.get_env() != profile.get_env() { continue } - profile = profile.opt_level(root_profile.get_opt_level()) - .debug(root_profile.get_debug()) - .rpath(root_profile.get_rpath()) - } + let profile = cx.profile(target); let prefer_dynamic = profile.is_for_host() || (crate_types.contains(&"dylib") && diff --git a/tests/support/paths.rs b/tests/support/paths.rs index 837963837..3039ce9df 100644 --- a/tests/support/paths.rs +++ b/tests/support/paths.rs @@ -61,7 +61,9 @@ impl PathExt for Path { if self.is_file() { try!(time_travel(self)); } else { + let target = self.join("target"); for f in try!(fs::walk_dir(self)) { + if target.is_ancestor_of(&f) { continue } if !f.is_file() { continue } try!(time_travel(&f)); } diff --git a/tests/test_cargo_cross_compile.rs b/tests/test_cargo_cross_compile.rs index a4d2ed024..3337e7d91 100644 --- a/tests/test_cargo_cross_compile.rs +++ b/tests/test_cargo_cross_compile.rs @@ -247,14 +247,15 @@ test!(plugin_to_the_max { version = "0.0.1" authors = [] "#) - .file("src/lib.rs", "pub fn baz() -> int { 1 }"); + .file("src/lib.rs", "pub fn baz() -> i32 { 1 }"); bar.build(); baz.build(); let target = alternate(); - assert_that(foo.cargo_process("build").arg("--target").arg(target), + assert_that(foo.cargo_process("build").arg("--target").arg(target).arg("-v"), execs().with_status(0)); - assert_that(foo.process(cargo_dir().join("cargo")).arg("build") + println!("second"); + assert_that(foo.process(cargo_dir().join("cargo")).arg("build").arg("-v") .arg("--target").arg(target), execs().with_status(0)); assert_that(&foo.target_bin(target, "foo"), existing_file()); diff --git a/tests/test_cargo_freshness.rs b/tests/test_cargo_freshness.rs index 699da047e..03107be6c 100644 --- a/tests/test_cargo_freshness.rs +++ b/tests/test_cargo_freshness.rs @@ -83,3 +83,54 @@ test!(modify_only_some_files { ", compiling = COMPILING, dir = path2url(p.root())))); assert_that(&p.bin("foo"), existing_file()); }); + +test!(rebuild_sub_package_then_while_package { + let p = project("foo") + .file("Cargo.toml", r#" + [package] + name = "foo" + authors = [] + version = "0.0.1" + + [dependencies.a] + path = "a" + [dependencies.b] + path = "b" + "#) + .file("src/lib.rs", "extern crate a; extern crate b;") + .file("a/Cargo.toml", r#" + [package] + name = "a" + authors = [] + version = "0.0.1" + [dependencies.b] + path = "../b" + "#) + .file("a/src/lib.rs", "extern crate b;") + .file("b/Cargo.toml", r#" + [package] + name = "b" + authors = [] + version = "0.0.1" + "#) + .file("b/src/lib.rs", ""); + + assert_that(p.cargo_process("build"), + execs().with_status(0)); + + File::create(&p.root().join("b/src/lib.rs")).unwrap().write_str(r#" + pub fn b() {} + "#).unwrap(); + + assert_that(p.process(cargo_dir().join("cargo")).arg("build").arg("-pb"), + execs().with_status(0)); + + File::create(&p.root().join("src/lib.rs")).unwrap().write_str(r#" + extern crate a; + extern crate b; + pub fn toplevel() {} + "#).unwrap(); + + assert_that(p.process(cargo_dir().join("cargo")).arg("build"), + execs().with_status(0)); +}); diff --git a/tests/test_cargo_test.rs b/tests/test_cargo_test.rs index e9e833a22..032154a16 100644 --- a/tests/test_cargo_test.rs +++ b/tests/test_cargo_test.rs @@ -1020,7 +1020,6 @@ test!(selective_testing { assert_that(p.process(cargo_dir().join("cargo")).arg("test") .arg("-p").arg("d1"), execs().with_status(0) - .with_stderr("") .with_stdout(format!("\ {compiling} d1 v0.0.1 ({dir}) {running} target[..]d1-[..] @@ -1035,7 +1034,6 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured\n assert_that(p.process(cargo_dir().join("cargo")).arg("test") .arg("-p").arg("d2"), execs().with_status(0) - .with_stderr("") .with_stdout(format!("\ {compiling} d2 v0.0.1 ({dir}) {running} target[..]d2-[..] @@ -1049,7 +1047,6 @@ test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured\n println!("whole"); assert_that(p.process(cargo_dir().join("cargo")).arg("test"), execs().with_status(0) - .with_stderr("") .with_stdout(format!("\ {compiling} foo v0.0.1 ({dir}) {running} target[..]foo-[..] -- 2.30.2